Home:ALL Converter>MassTransit In-Memory Outbox in Saga

MassTransit In-Memory Outbox in Saga

Ask Time:2020-02-24T01:27:35         Author:Jakub Milik

Json Formatter

We did some load tests on a Saga with a In-Memory outbox. During those tests we simulated different types of failures: application restarts, infrastructure restart, message broker restart etc.

We noticed, that some saga instances did not finish and we had a bunch of errors: Automatonymous.NotAcceptedStateMachineException: ... {SomeEvent}: Not accepted in state {SomeState}

After some debuging we isolated the problem. I'll try to describe it using this sample code:

public class OrderStateMachine : MassTransitStateMachine<Order>
{
    public OrderStateMachine()
    {
        InstanceState(x => x.CurrentState);

        During(Initial, 
            When(Create).TransitionTo(New));

        During(New,
            When(AddItem)
                .Then(x => x.Instance.Items.Add(x.Data.Name)),

            When(Submit)
                .ThenAsync(async x =>
                {
                    // do something
                    await x.Publish(new SendEmail {Text = $"Order submitted. {x.Instance.Summary}"});
                })
                .TransitionTo(Submitted));

        During(Submitted,
            When(Accept)
                .ThenAsync(async x =>
                {
                    // do something
                    await x.Publish(new SendEmail {Text = $"Order accepted. {x.Instance.Summary}"});
                })
                .Finalize());

        SetCompletedWhenFinalized();
    }

    public State New { get; private set; }
    public State Submitted { get; private set; }

    public Event<Create> Create { get; private set; }
    public Event<AddItem> AddItem { get; private set; }
    public Event<Submit> Submit { get; private set; }
    public Event<Accept> Accept { get; private set; }
}

public class Order : SagaStateMachineInstance
{
    public Guid CorrelationId { get; set; }
    public string CurrentState { get; set; }
    public IList<string> Items { get; set; } = new List<string>();

    public string Summary => $"Items: {string.Join(", ", Items)}";
}

public class Create : CorrelatedBy<Guid>
{
    public Guid CorrelationId { get; set; }
}

public class AddItem : CorrelatedBy<Guid>
{
    public Guid CorrelationId { get; set; }
    public string Name { get; set; }
}

public class Submit : CorrelatedBy<Guid>
{
    public Guid CorrelationId { get; set; }
}

public class Accept : CorrelatedBy<Guid>
{
    public Guid CorrelationId { get; set; }
}

public class SendEmail
{
    public string Text { get; set; }
}

This is what happens:

  1. During Order in state New, we handle Submit event, do some changes in order, publish SendEmail event and transition to Submitted state.
  2. Order state is successfully persisted to DB
  3. Application crashes (i.e force restart) before it sends the message outbox.
  4. Application is restarted, Submit is delivered the second time, Order is in state Submitted and we get an Exception Automatonymous.NotAcceptedStateMachineException: ... Submit: Not accepted in state Submitted

What if this happens in state Submitted during Accept event handling? My assumption:

  1. During Order in state Submitted, we handle Accept event, do some changes in order, publish SendEmail event and Finalize Order
  2. Order state is removed form DB, because it's finalized and configured to CompletedWhenFinalized
  3. Application crashes (i.e force restart) before it sends the message outbox.
  4. Application is restarted, Accept is delivered the second time, Order is no longer in DB, we lost all information about it... what happens now?

What is a best solution to handle situations like this? I've read Chris's great article about In-Memory Outbox, but don't understand how can the message be handled during redelivery when Saga is in a state where it no longer handles that message. Of course we can handle the redelivered event in the next state with some tricky logic, but it seems pretty cumbersome. Our Saga is much more complex then the provided sample.

Maybe a transaction which commits after all messages from outbox have been sent, would be a solution? Can the Transaction Outbox be somehow configured with a Saga?

Author:Jakub Milik,eproduced under the CC 4.0 BY-SA copyright license with a link to the original source and this disclaimer.
Link to original article:https://stackoverflow.com/questions/60364891/masstransit-in-memory-outbox-in-saga
yy